package ngram.util;

import com.esotericsoftware.kryo.io.ByteBufferOutputStream;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.CountingInputStream;
import exceptions.NegativeNumberException;
import function.unary.UnaryFunction;
import function.unary.UnaryPredicate;
import it.unimi.dsi.io.ByteBufferInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.UnsupportedEncodingException;
import list.functions.ListFunctions;
import processorIO.FileIO;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
import ngram.exception.NoNGramFilesFoundException;
import processorIO.FilePath;
import tuples.Pair;
import unit_conversion.BinaryConversion;

/**
 * A set of IO methods that are specifically optimized or modified to suit
 * the needs of the NGram files.
 * @author g1wshimu
 */
public class IOUtil {

    public static final Charset ENCODING = Charset.forName("UTF-8");
    public static final ObjectMapper JSON_UTIL = new ObjectMapper();

    private IOUtil() {
    }

    /**
     * Returns a memory mapped input stream if possible. Otherwise, return
     * a regular FileInputStream. IMPORTANT: the RandomAccessFile associated
     * with the returned memory mapped input stream is also returned. One 
     * must close the RandomAccessFile manually (closing the stream does 
     * not close the RandomAccessFile). It is recommended to override the
     * default close() method of the wrapping stream to also close the 
     * RandomAccessFile.
     * 
     * @param url
     * @return
     * @throws FileNotFoundException
     * @throws IOException 
     */
    private static Pair<InputStream, RandomAccessFile> getMemoryMappedInputStream(String url)
            throws FileNotFoundException, IOException {
        final InputStream stream;
        if (BinaryConversion.bytesToMb(new File(url).length()) < 500) {
            final RandomAccessFile randomAccessFile = new RandomAccessFile(url, "r");
            stream = ByteBufferInputStream.map(randomAccessFile.getChannel());
            return new Pair<>(stream, randomAccessFile);
        } else {
            // Use a regular file input stream if the file is too big
            stream = new FileInputStream(url);
            return new Pair<>(stream, null);
        }
    }

    public static BufferedReader getBufferedReader(String url)
            throws FileNotFoundException, UnsupportedEncodingException, IOException {
        final Pair<InputStream, RandomAccessFile> pair = getMemoryMappedInputStream(url);
        return new BufferedReader(new InputStreamReader(
                pair.getFirst(), ENCODING)) {

            @Override
            public void close() throws IOException {
                super.close();
                if (pair.getSecond() != null) {
                    pair.getSecond().close();
                }
            }
        };
    }

    public static CountingDataInputStream getCountableInputStream(String url)
            throws FileNotFoundException, IOException {
        final Pair<InputStream, RandomAccessFile> pair = getMemoryMappedInputStream(url);

        return new CountingDataInputStream(new CountingInputStream(pair.getFirst())) {

            @Override
            public void close() throws IOException {
                super.close();
                if (pair.getSecond() != null) {
                    pair.getSecond().close();
                }
            }
        };
    }

    public static DataInputStream getDataInputStream(String url)
            throws FileNotFoundException, IOException {
        final Pair<InputStream, RandomAccessFile> pair = getMemoryMappedInputStream(url);
        return new DataInputStream(pair.getFirst()) {

            @Override
            public void close() throws IOException {
                super.close();
                if (pair.getSecond() != null) {
                    pair.getSecond().close();
                }
            }
        };
    }

    public static DataOutputStream getDataOutputStream(String url)
            throws FileNotFoundException {
        return new DataOutputStream(new BufferedOutputStream(
                new FileOutputStream(url)));
    }

    public static void writeUTF(DataOutputStream writer, String str)
            throws IOException {
        byte[] out = str.getBytes(IOUtil.ENCODING);
        writer.writeShort(out.length);
        writer.write(out);
    }

    public static String readUtf(DataInputStream reader) throws IOException,
            NegativeNumberException {
        int length;
        try {
            length = reader.readShort();
        } catch (EOFException e) {
            return null;
        }

        NegativeNumberException.validate("length", length);

        byte[] in = new byte[length];
        try {
            reader.readFully(in);
        } catch (EOFException e) {
            return null;
        }
        return new String(in, IOUtil.ENCODING);
    }

    public static void skip(DataInputStream reader, final int bytesToSkip)
            throws EOFException, IOException {
        int actuallySkipped = 0;
        while (actuallySkipped < bytesToSkip) {
            int bytesToRead = bytesToSkip - actuallySkipped;
            long skipped = reader.skipBytes(bytesToRead);

            if (skipped > 0) {
                actuallySkipped += skipped;
            } else {
                // Might throw EOF
                reader.readFully(new byte[bytesToRead]);
            }
        }
    }

    public static String readLine(DataInputStream reader)
            throws IOException {
        char ch;
        StringBuilder b = new StringBuilder(50);
        while ((ch = (char) reader.readByte()) != '\n') {
            b.append(ch);
        }
        return b.toString();
    }

    /**
     * Given a file, returns a list of files that contain the 
     * word "google" in its file name.
     * 
     * @param file
     * @return
     * @throws IOException 
     */
    public static List<String> getFiles(final File file) throws
            FileNotFoundException, NoNGramFilesFoundException, IOException {
        final String path = file.getAbsolutePath();

        // Find the name of all the files in the file
        List<String> list = Arrays.asList(FileIO.listFiles(path));

        // Keep only files that contain in its name "google"
        list = ListFunctions.filter(new UnaryPredicate<String>() {

            @Override
            public Boolean evaluate(String arg) {
                return arg.contains("google");
            }
        }, list);

        if (list.isEmpty()) {
            throw new NoNGramFilesFoundException(file);
        }

        // Prepend the file to all the files
        list = ListFunctions.map(new UnaryFunction<String, String>() {

            @Override
            public String evaluate(String fileName) {
                return FilePath.join(path, fileName);
            }
        }, list);

        return list;
    }

    public static DataOutput wrapByteBufferAsDataOutput(ByteBuffer buffer) {
        ByteBufferOutputStream bufferStream = new ByteBufferOutputStream(buffer);
        return new DataOutputStream(bufferStream);
    }
}
